﻿using System;
using System.Collections.Generic;
using dnAnalytics.Properties;

namespace dnAnalytics.Statistics
{
    /// <summary>
    /// Extension methods to return basic statistics on set of data.
    /// </summary>
    public static class Statistics
    {
        /// <summary>
        /// Calculates the sample mean.
        /// </summary>
        /// <param name="data">The data to calculate the mean of.</param>
        /// <returns>The mean of the sample.</returns>
        public static double Mean(this IEnumerable<double> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }
            double mean = 0;
            int m = 0;
            foreach (var item in data)
            {
                mean += (item - mean)/++m;
            }
            return mean;
        }


        /// <summary>
        /// Calculates the sample mean.
        /// </summary>
        /// <param name="data">The data to calculate the mean of.</param>
        /// <returns>The mean of the sample.</returns>
        public static double Mean(this IEnumerable<double?> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }
            double mean = 0;
            int m = 0;
            foreach (var item in data)
            {
                if (item.HasValue)
                {
                    mean += (item.Value - mean)/++m;
                }
            }
            return mean;
        }

        /// <summary>
        /// Calculates the sample median.
        /// </summary>
        /// <param name="data">The data to calculate the median of.</param>
        /// <returns>The median of the sample.</returns>
        public static double Median(this IEnumerable<double> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }

            List<double> dataArray = new List<double>(data);
            dataArray.Sort();

            int index = dataArray.Count/2 - 1;
            if (dataArray.Count%2 == 0)
            {
                return (dataArray[index] + dataArray[index + 1])/2.0;
            }
            return dataArray[index + 1];
        }

        /// <summary>
        /// Calculates the sample median.
        /// </summary>
        /// <param name="data">The data to calculate the median of.</param>
        /// <returns>The median of the sample.</returns>
        public static double Median(this IEnumerable<double?> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }

            List<double> nonNull = new List<double>();
            foreach (double? value in data)
            {
                if (value.HasValue)
                {
                    nonNull.Add(value.Value);
                }
            }
            if (nonNull.Count == 0)
            {
                throw new ArgumentException(Resources.CollectionEmpty, "data");
            }
            return nonNull.Median();
        }

        /// <summary>
        /// Calculates the sample variance.
        /// </summary>
        /// <param name="data">The data to calculate the variance of.</param>
        /// <returns>The variance of the sample.</returns>
        public static double Variance(this IEnumerable<double> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }
            double variance = 0;
            double t = 0;
            int j = 0;

            IEnumerator<double> iterator = data.GetEnumerator();
            if (iterator.MoveNext())
            {
                j++;
                t = iterator.Current;
            }

            while (iterator.MoveNext())
            {
                j++;
                double xi = iterator.Current;
                t += xi;
                double diff = j*xi - t;
                variance += (diff*diff)/(j*(j - 1));
            }

            return variance/(j - 1);
        }

        /// <summary>
        /// Calculates the sample variance.
        /// </summary>
        /// <param name="data">The data to calculate the variance of.</param>
        /// <returns>The variance of the sample.</returns>
        public static double Variance(this IEnumerable<double?> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }
            double variance = 0;
            double t = 0;
            int j = 0;

            IEnumerator<double?> iterator = data.GetEnumerator();

            while (true)
            {
                bool hasNext = iterator.MoveNext();
                if (!hasNext)
                {
                    break;
                }
                if (iterator.Current.HasValue)
                {
                    j++;
                    t = iterator.Current.Value;
                    break;
                }
            }

            while (iterator.MoveNext())
            {
                if (iterator.Current.HasValue)
                {
                    j++;
                    double xi = iterator.Current.Value;
                    t += xi;
                    double diff = j*xi - t;
                    variance += (diff*diff)/(j*(j - 1));
                }
            }

            return variance/(j - 1);
        }


        /// <summary>
        /// Calculates the sample standard deviation.
        /// </summary>
        /// <param name="data">The data to calculate the standard deviation of.</param>
        /// <returns>The standard deviation of the sample.</returns>
        public static double StandardDeviation(this IEnumerable<double> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }

            return System.Math.Sqrt(Variance(data));
        }

        /// <summary>
        /// Calculates the sample standard deviation.
        /// </summary>
        /// <param name="data">The data to calculate the standard deviation of.</param>
        /// <returns>The standard deviation of the sample.</returns>
        public static double StandardDeviation(this IEnumerable<double?> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }

            return System.Math.Sqrt(Variance(data));
        }

        /// <summary>
        /// Returns the minimum value in the sample data.
        /// </summary>
        /// <param name="data">The sample data.</param>
        /// <returns>The minimum value in the sample data.</returns>
        public static double Minimum(this IEnumerable<double?> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }

            double min = double.MaxValue;
            int count = 0;
            foreach (double? d in data)
            {
                if (d.HasValue)
                {
                    min = System.Math.Min(min, d.Value);
                    count++;
                }
            }
            if (count == 0)
            {
                throw new ArgumentException(Resources.CollectionEmpty, "data");
            }
            return min;
        }

        /// <summary>
        /// Returns the maximum value in the sample data.
        /// </summary>
        /// <param name="data">The sample data.</param>
        /// <returns>The maximum value in the sample data.</returns>
        public static double Maximum(this IEnumerable<double?> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }

            double max = double.MinValue;
            int count = 0;
            foreach (double? d in data)
            {
                if (d.HasValue)
                {
                    max = System.Math.Max(max, d.Value);
                    count++;
                }
            }
            if (count == 0)
            {
                throw new ArgumentException(Resources.CollectionEmpty, "data");
            }
            return max;
        }

        /// <summary>
        /// Returns the minimum value in the sample data.
        /// </summary>
        /// <param name="data">The sample data.</param>
        /// <returns>The minimum value in the sample data.</returns>
        public static double Minimum(this IEnumerable<double> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }

            double min = double.MaxValue;
            int count = 0;
            foreach (double d in data)
            {
                min = System.Math.Min(min, d);
                count++;
            }

            if (count == 0)
            {
                throw new ArgumentException(Resources.CollectionEmpty, "data");
            }
            return min;
        }

        /// <summary>
        /// Returns the maximum value in the sample data.
        /// </summary>
        /// <param name="data">The sample data.</param>
        /// <returns>The maximum value in the sample data.</returns>
        public static double Maximum(this IEnumerable<double> data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }

            double max = double.MinValue;
            int count = 0;
            foreach (double d in data)
            {
                max = System.Math.Max(max, d);
                count++;
            }

            if (count == 0)
            {
                throw new ArgumentException(Resources.CollectionEmpty, "data");
            }
            return max;
        }
    }
}